查看原文
其他

如何用 Python 和 fast.ai 做图像深度迁移学习?

王树义老师 玉树芝兰 2019-05-05

本文带你认识一个优秀的新深度学习框架,了解深度学习中最重要的3件事。

框架

看到这个题目,你可能会疑惑:

老师,你不是讲过如何用深度学习做图像分类了吗?迁移学习好像也讲过了啊!

说得对!我要感谢你对我专栏的持续关注。我确实讲过深度学习做图像分类,以及迁移学习这两项内容。

写这篇文章,是因为最近因为科研的关系,发现了 fast.ai 这款框架。我希望把它介绍给你。

你可能会不解,之前介绍过的 TuriCreate, Tensorflow, tflearn 和 Keras 好像都挺好用的啊!

我想问问,你在实际的科研工作里,用过哪一个呢?

大多数的读者,只怕基本上都没真正用它们跑过实际的任务。

为什么呢?

因为对普通用户(例如我经常提到的“文科生”),这些框架要么用起来很简单,但是功能不够强大;要么功能很强大,但是不够易用。

例如苹果的 TuriCreate ,我给你演示过,直接零基础上手都没问题。但当你希望对模型进行构造调整的时候,马上就会发现困难重重。因为其专长在于快速产生模型,并且部署到苹果移动设备,因此文档里面底层细节的介绍是有欠缺的。而且有些模型,非苹果平台目前还不能兼容。

至于某著名框架,直到推出3年后,在各方压力下,不得已才把好用的 Eager Execution 作为主要使用模式。其间充分体现了那种技术人员独有的傲慢和固执。另外,就连程序员和数据科学家们都把吐槽“看不懂”它的官方文档当作了家常便饭。这些轶事,由于公开发布会招致口水仗,所以我只写在了知识星球专属语雀团队《发现了一套非常棒的(该框架名称)视频教程》一文中。感兴趣的话,不妨去看看。

原本我认为, Keras 已经是把功能和易用性做到了最佳平衡了。直到我看到了 Jeremy Howard,也就是 fast.ai 创始人提出的评判标准——如果一个深度学习框架需要写个教程给你,那它的易用性还不够好。

我看了之后,可以用感动来形容。

Jeremy 说这话,不是为了夸自己——因为他甚至做了个 MOOC 出来。他自己评价,说目前 fast.ai 的易用性依然不算成功。但在我看来, fast.ai 是目前把易用性和功能都做到了极致的深度学习框架。

它的门槛极低。如同 TuriCreate 一样,你可以很轻易用几句话写个图片分类模型出来,人人都能立即上手。

它的天花板又很高。因为它只是个包裹了 Pytorch 的代码库。

你可能也听说了,在过去的一年里,Pytorch 在学术界大放异彩,就是因为它的门槛对于科研人员来说,已经足够友好了。如果你有需求,可以非常方便地通过代码的修改和复用,敏捷构造自己的深度学习模型。

这种积木式的组合方式,使得许多新论文中的模型,可以第一时间被复现验证。如果你在这个过程中有了自己的灵感和心得,可以马上实践。

且慢,fast.ai 的作者不是已经做了自己的 MOOC 了吗?那写这篇文章,岂不是多此一举?

不是的。

首先,作者每年迭代一个 MOOC 的版本,因为 MOOC 一共包括三门课程,分别是:

  • Practical Deep Learning for Coders

  • Cutting Edge Deep Learning for Coders

  • Introduction to Machine Learning for Coders

但现在你能看到的深度学习基础课,还是去年录的。今年10月,伴随着 Pytorch 1.0 的推出, fast.ai 做了一次显著的大版本(1.0)更新。如果你去看去年的课程,会发现和目前的 fast.ai 代码有很多区别。在完成同一个功能时,你愿意再跑去学旧的过时内容吗?特别是,如果搞混了,还很容易出错。

可是,想看到这个版本课程的免费视频,你至少得等到明年1月。因为目前正式学员们也才刚刚开课。

而且,那视频,也是英文的。

正因如此,我觉得有必要给你讲讲,如何用最新的 fast.ai 1.0 版本,来完成图像深度迁移学习。

数据

Jeremy 在 MOOC 中提到,如果你打算让机器通过数据来学习,你需要提供3样东西给它,分别是:

  • 数据(Data)

  • 模型结构(Architecture)

  • 损失度量(Loss Metrics)

模型结构,是根据你的具体问题走的。例如说,你需要让机器做图片分类,那么就需要使用卷积神经网络(Convolutional Neural Network)来表征图片上的像素信息构成的特征。如果你需要做自然语言处理,那么就可以使用循环神经网络(Recurrent Neural Network)来捕捉文本或者字符的顺序关联信息。

损失衡量,是指你提供一个标准,衡量机器对某项任务的处理水平。例如说对于分类效果如何,你可以使用交叉熵(Binary Cross Entropy)来评判。这样,机器会尝试最小化损失结果,从而让分类表现越来越好。

至于数据,因为我们这里的任务是做分类。因此需要有标注的训练数据。

我已经把本文需要用到的数据放到了这个 github 项目上。

打开其中的 imgs 文件夹,你会看见3个子文件夹,分别对应训练(train),验证(valid)和测试(test)。

打开 train 文件夹看看。

你没猜错,我们用的图片还是哆啦A梦(doraemon)和瓦力(walle)。

因为这样不仅可以保持教程的一惯性,而且也可以保证结果对比的公平。

打开哆啦A梦的目录看看:

展示其中第一个文件内容。

好熟悉,是不是?

你可以浏览一下其他的哆啦A梦照片,然后别忘了去瓦力的文件夹里面扫上一眼。

这就是我们的数据集了。

环境

为了运行深度学习代码,你需要一个 GPU 。但是你不需要去买一个,租就好了。最方便的租用方法,就是云平台。

fast.ai 官方,给出了以下5种云计算平台使用选项:

  • Paperspace Gradient

  • Salamander

  • SageMaker

  • Google Compute Platform

  • Amazon Web Services EC2

其中,我推荐你使用的,是 Google Compute Platform 。原因很简单,首先它成本低,每小时只需要 0.38 美元。更重要的是,如果你是新用户, Google 会先送给你300美金,1年内有效。算算看,这够你运行多久深度学习?

原先,fast.ai 上面的设置 Google Compute Platform 教程写得很简略。于是我写了个一步步的教程,请使用这个链接访问。

不过,我发现 fast.ai 的迭代速度简直惊人,短短几天时间,新的教程就出来了,而且详尽许多。因此你也可以点击这里查看官方的教程。其中如果有跳步,你可以回看我的教程,作为补充。

因此,Google Compute Platform 中间步骤,咱们就不赘述了。当你的终端里面出现这样的提示的时候,就证明一切准备工作都就绪了。

下面,你需要下载刚刚在 github 上面的代码和数据集。

git clone https://github.com/wshuyi/demo-image-classification-fastai.git

之后,就可以呼叫 jupyter 出场了。

jupyter lab

注意因为你是在 Google Compute Platform 云端执行 jupyter ,因此浏览器不会自动弹出。

你需要打开 Firefox 或者 Chrome,在其中输入这个链接(http://localhost:8080/lab?)。

打开左侧边栏里面的 demo.ipynb

本教程全部的代码都在这里了。当然,你如果比较心急,可以选择执行Run->Run All Cells,查看全部运行结果。

但是,跟之前一样,我还是建议你跟着教程的说明,一步步执行它们。以便更加深刻体会每一条语句的含义。

载入

我们先要载入数据。第一步是从 fast.ai 读入一些相关的功能模块。

from fastai import *
from fastai.vision import *
from fastai.core import *

接着,我们需要设置数据所在文件夹的位置,为 imgs 目录。

执行:

path = Path('imgs')

下面,我们让 fast.ai 帮我们载入全部的数据。这时我们调用 ImageDataBunch 类的 from_folder 函数,结果存储到 data 中:

data = ImageDataBunch.from_folder(path, test='test', ds_tfms=get_transforms(), size=224)

注意这里,我们不仅读入了数据,还顺手做了2件事:

  • 我们进行了数据增强(augmentation),也就是对数据进行了翻转、拉伸、旋转,弄出了很多“新”训练数据。这样做的目的,是因为数据越多,越不容易出现过拟合(over-fitting),也就是模型死记硬背,蒙混考试,却没有抓住真正的规律。

  • 我们把图片大小进行了统一,设置成了 224 x 224 ,这样做的原因,是我们需要使用迁移学习,要用到预训练模型。预训练模型是在这样大小的图片上面训练出来的,因此保持大小一致,效果更好。

下面,检查一下数据载入是否正常:

data.show_batch(rows=3, figsize=(10,10))

没问题。图片和标记都是正确的。

训练

用下面这一条语句,我们把“数据”、“模型结构”和“损失度量”三样信息,一起喂给机器。

learn = ConvLearner(data, models.resnet34, metrics=accuracy)

数据就不说了,模型我们采用的是 resnet34 这样一个预训练模型作为基础架构。至于损失度量,我们用的是准确率(accuracy)。

你可能会纳闷,这就完了?不对呀!

没有告诉模型类别有几个啊,没有指定任务迁移之后接续的几个层次的数量、大小、激活函数……

对,不需要。

因为 fast.ai 根据你输入的上述“数据”、“模型结构”和“损失度量”信息,自动帮你把这些闲七杂八的事情默默搞定了。

下面,你需要用一条指令来训练它:

learn.fit_one_cycle(1)

注意,这里我们要求 fast.ai 使用 one cycle policy 。如果你对细节感兴趣,可以点击这个链接了解具体内容。

5秒钟之后,训练结束。

验证集准确率是,100%。

注意,你“拿来”的这个 resnet34 模型当初做训练的时候,可从来没有见识过哆啦A梦或者瓦力。

看了100多张形态各异,包含各种背景噪声的图片,它居然就能 100% 准确分辨了。

之前我们讲过机器学习的可解释性很重要。没错,fast.ai 也帮我们考虑到了这点。

preds,y = learn.get_preds()
interp = ClassificationInterpretation(data, preds, y, loss_class=nn.CrossEntropyLoss)

执行上面这两行语句,不会有什么输出。但是你手里有了个解释工具。

我们来看看,机器判断得最不好的9张图片都有哪些?

interp.plot_top_losses(9, figsize=(10,10))

因为准确率已经 100% 了,所以单看数值,你根本无法了解机器判断不同照片的时候,遇到了哪些问题。但是这个解释器却可以立即让你明白,哪些图片,机器处理起来,底气(信心)最为不足。

我们还能让解释器做个混淆矩阵出来:

interp.plot_confusion_matrix()

不过这个混淆矩阵好像没有什么意思。反正全都判断对了。

评估

我们的模型,是不是已经完美了?

不好说。

因为我们刚才展示的,只是验证集的结果。这个验证集,机器在迭代模型参数的时候每一回都拿来尝试。所以要检验最为真实的效能,我们需要让机器看从来没有看到过的图片。

你可以到 test 目录下面,看看都有什么。

注意这里一共6张图片,3张哆啦A梦的,3张瓦力的。

这次,我们还会使用刚才用过的 get_preds 函数。不过区别是,我们把 is_test 标记设置为 True,这样机器就不会再去验证集里面取数据了,而是看测试集的。

preds,y = learn.get_preds(is_test=True)

注意目录下面看到的文件顺序,是依据名称排列的。但是 fast.ai 读取数据的时候,其实是做了随机洗牌(randomized shuffling)。我们得看看实际测试集里面的文件顺序。

data.test_dl.dl.dataset.ds.x

好了,我们自己心里有数了。下面就看看机器能不能都判断正确了。

preds

这都啥玩意儿啊?

别着急,这是模型预测时候,根据两个不同的分类,分别给出的倾向数值。数值越大,倾向程度越高。

左侧一列,是哆啦A梦;右侧一列,是瓦力。

我们用 np.argmax 函数,把它简化一些。

np.argmax(preds, axis=1)

这样一来,看着就清爽多了。

我们来检查一下啊:瓦力,瓦力,哆啦A梦,哆啦A梦,哆啦A梦,哆啦A梦……

不对呀!

最后这一张,walle.113.jpg,不应该判断成瓦力吗?

打开看看。

哦,难怪。另一个机器人也出现在图片中,圆头圆脑的,确实跟哆啦A梦有相似之处。

要不,就这样了?

微调

那哪儿行?!

我们做任务,要讲究精益求精啊。

遇到错误不要紧,我们尝试改进模型。

用的方法,叫做微调(fine-tuning)。

我们刚刚,不过是移花接木,用了 resnet34 的身体,换上了一个我们自定义的头部层次,用来做哆啦A梦和瓦力的分辨。

这个训练结果,其实已经很好了。但是既然锁定了“身体”部分的全部参数,只训练头部,依然会遇到判断失误。那我们自然想到的,就应该是连同“身体”,一起调整训练了。

但是这谈何容易?

你调整得动作轻微,那么效果不会明显;如果你调整过了劲儿,“身体”部分的预训练模型通过海量数据积累的参数经验,就会被破坏掉。

两难啊,两难!

好在,聪明的研究者提出了一个巧妙的解决之道。这非常符合我们不只一次提及的“第一性原理”,那就是返回到事情的本源,问出一句:

谁说调整的速度,要全模型都一致?!

深度卷积神经网络,是一个典型的层次模型。

模型靠近输入的地方,捕获的是底层的特征。例如边缘形状等。

模型靠近输出的地方,捕获的是高层特征,例如某种物体的形貌。

对于底层特征,我们相信哆啦A梦、瓦力和原先训练的那些自然界事物,有很多相似之处,因此应该少调整。

反之,原先模型用于捕获猫、狗、兔子的那些特征部分,我们是用不上的,因此越靠近输出位置的层次,我们就应该多调整。

这种不同力度的调整,是通过学习速率(learning rate)来达成的。具体到我们的这种区分,专用名词叫做“歧视性学习速率”(discriminative learning rate)。

你可能想放弃了,这么难!我不玩儿了!

且慢,看看 fast.ai 怎么实现“歧视性学习速率”。

learn.unfreeze()
learn.fit_one_cycle(3, slice(1e-5,3e-4))

对,只需在这里指定一下,底层和上层,选择什么不同的起始速率。搞定。

没错,就是这么不讲道理地智能化

这次,训练了3个循环(cycle)。

注意,虽然准确率没有变化(一直是100%,也不可能提升了),但是损失数值,不论是训练集,还是验证集上的,都在减小。

这证明模型在努力地学东西。

你可能会担心:这样会不会导致过拟合啊?

看看就知道了,训练集上的损失数值,一直高于验证集,这就意味着,没有过拟合发生的征兆。

好了,拿着这个微调优化过后的模型,我们再来试试测试集吧。

首先我们强迫症似地看看测试集文件顺序有没有变化:

data.test_dl.dl.dataset.ds.x

既然没有变,我们就放心了。

下面我们执行预测:

preds,y = learn.get_preds(is_test=True)

然后,观察结果:

np.argmax(preds, axis=1)

如你所见,这次全部判断正确。

可见,我们的微调,是真实有用的。

小结

本文为你介绍了如何用 fast.ai 1.0 框架进行图像深度迁移学习。可以看到, fast.ai 不仅简洁、功能强大,而且足够智能化。所有可以帮用户做的事情,它全都替你代劳。作为研究者,你只需要关注“数据”、“模型结构”和“损失度量”这3个关键问题,以改进学习效果。

我希望你不要满足于把代码跑下来。用你获得的300美金,换上自己的数据跑一跑,看看能否获得足够满意的结果。

祝(深度)学习愉快!

喜欢请点赞和打赏。还可以微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)。

如果你对 Python 与数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。

由于微信公众号外部链接的限制,文中的部分链接可能无法正确打开。如有需要,请点击文末的“阅读原文”按钮,访问可以正常显示外链的版本。

知识星球入口在这里:


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存